package cc.shacocloud.mirage.loader;

import cc.shacocloud.mirage.loader.jar.Handler;
import cc.shacocloud.mirage.loader.jar.JarEntry;
import cc.shacocloud.mirage.loader.jar.JarFile;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.FileSystem;
import java.nio.file.*;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * Mirage Jar 启动器
 *
 * @author 思追(shaco)
 */
public class MirageJarLauncher implements Launcher {
    
    private static final int BUFFER_SIZE = 32 * 1024;
    private static final FileAttribute<?>[] NO_FILE_ATTRIBUTES = {};
    private static final EnumSet<PosixFilePermission> DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
            PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
    private static final EnumSet<PosixFilePermission> FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
            PosixFilePermission.OWNER_WRITE);
    private final File file;
    private final Manifest manifest;
    private final JarFile jarFile;
    @Setter
    private Path unpackDirectory;
    
    public MirageJarLauncher() throws Exception {
        this(null);
    }
    
    public MirageJarLauncher(File file) throws Exception {
        this.file = Objects.isNull(file) ? getCurrentJarFile() : file;
        this.jarFile = new JarFile(this.file);
        this.manifest = this.jarFile.getManifest();
    }
    
    @Override
    public void run(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        Launcher.super.run(args);
    }
    
    @Override
    public ClassLoader createClassLoader() throws Exception {
        Attributes mainAttributes = this.manifest.getMainAttributes();
        
        List<URL> urls = new ArrayList<>();
        
        // 依赖路径
        String libIndexPath = mainAttributes.getValue(LIB_INDEX_ATTRIBUTE);
        try (InputStream inputStream = jarFile.getInputStream(jarFile.getJarEntry(libIndexPath));
             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            reader.lines().map(this::parseClasspath).forEach(urls::add);
        }
        
        // 类路径
        String classesBasePath = mainAttributes.getValue(CLASSES_ATTRIBUTE);
        urls.add(parseClasspath(classesBasePath));
        
        URL[] urlArr = urls.stream().filter(Objects::nonNull).toArray(URL[]::new);
        return new MirageClassLoader(false, jarFile, urlArr, getClass().getClassLoader());
    }
    
    @Override
    public String getMainClassName() throws Exception {
        Attributes mainAttributes = this.manifest.getMainAttributes();
        String mainClassName = mainAttributes.getValue(START_CLASS_ATTRIBUTE);
        if (Objects.isNull(mainClassName)) {
            throw new IllegalStateException("无法从清单文件中获取启动类名称！");
        }
        return mainClassName;
    }
    
    /**
     * 获取当前运行 jar 的 File 对象
     */
    public File getCurrentJarFile() throws Exception {
        ProtectionDomain domain = getClass().getProtectionDomain();
        CodeSource codeSource = (domain != null) ? domain.getCodeSource() : null;
        URL location = (codeSource != null) ? codeSource.getLocation() : null;
        
        if (location != null) {
            URLConnection connection = location.openConnection();
            if (connection instanceof JarURLConnection) {
                try (java.util.jar.JarFile jarFile = ((JarURLConnection) connection).getJarFile()) {
                    String name = jarFile.getName();
                    int separator = name.indexOf("!/");
                    if (separator > 0) {
                        name = name.substring(0, separator);
                    }
                    
                    return new File(name);
                }
            } else {
                URI locationUri = location.toURI();
                
                String path = locationUri.getSchemeSpecificPart();
                if (path == null) {
                    throw new IllegalStateException("无法获取当前执行 jar 的路径");
                }
                
                File file = new File(path);
                if (!file.exists()) {
                    throw new IllegalStateException("不存在的jar文件路径： " + path);
                }
                
                if (file.isFile() && file.getName().endsWith(".jar")) {
                    return file;
                }
            }
        }
        
        throw new IllegalStateException("无法获取当前执行 jar 的文件对象！");
    }
    
    /**
     * 解析类路径，使用 {@link URL}
     *
     * @param path 是 jar 文件或目录，如果不存在返回 null
     */
    @Nullable
    public URL parseClasspath(@NotNull String path) {
        try {
            // 内嵌 jar
            if (path.endsWith(".jar")) {
                JarEntry jarEntry = jarFile.getJarEntry(path);
                if (Objects.isNull(jarEntry)) return null;
                
                String name = jarEntry.getName();
                if (name.lastIndexOf('/') != -1) {
                    name = name.substring(name.lastIndexOf('/') + 1);
                }
                
                // 将内嵌jar解压到指定目录
                Path unpackDirectoryPath = getUnpackDirectory().resolve(name);
                if (!Files.exists(unpackDirectoryPath) || Files.size(unpackDirectoryPath) != jarEntry.getSize()) {
                    unpack(jarEntry, unpackDirectoryPath);
                }
                
                return unpackDirectoryPath.toUri().toURL();
            }
            
            path = path.startsWith("/") ? path : "/" + path;
            return new URL("jar", null, -1, file.toURI().toURL() + "!" + path, new Handler(jarFile));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 获取当前解压目录，如果未自定义则使用临时目录作为父目录
     *
     * @see #createUnpackDirectory(Path)
     */
    protected Path getUnpackDirectory() {
        if (this.unpackDirectory == null) {
            Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"));
            this.unpackDirectory = createUnpackDirectory(tempDirectory);
            this.unpackDirectory.toFile().deleteOnExit();
        }
        return this.unpackDirectory;
    }
    
    /**
     * 创建解压目录
     */
    @NotNull
    private Path createUnpackDirectory(@NotNull Path parent) {
        String fileName = Paths.get(this.jarFile.getName()).getFileName().toString();
        fileName = fileName.replace(".jar", "");
        Path unpackDirectory = parent.resolve(fileName + "-mirage-libs-" + UUID.randomUUID());
        
        try {
            Files.createDirectories(unpackDirectory, getFileAttributes(unpackDirectory.getFileSystem(), DIRECTORY_PERMISSIONS));
            return unpackDirectory;
        } catch (IOException e) {
            throw new IllegalStateException(String.format("无法在目录 %s 中创建解解压目录", parent), e);
        }
    }
    
    private FileAttribute<?>[] getFileAttributes(@NotNull FileSystem fileSystem, EnumSet<PosixFilePermission> ownerReadWrite) {
        if (!fileSystem.supportedFileAttributeViews().contains("posix")) {
            return NO_FILE_ATTRIBUTES;
        }
        return new FileAttribute<?>[]{PosixFilePermissions.asFileAttribute(ownerReadWrite)};
    }
    
    /**
     * 解压jar条目
     */
    private void unpack(@NotNull JarEntry entry, @NotNull Path path) throws IOException {
        Files.createDirectories(path.getParent(), getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS));
        Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS));
        path.toFile().deleteOnExit();
        
        try (InputStream inputStream = this.jarFile.getInputStream(entry);
             OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            outputStream.flush();
        }
    }
    
}
